Skip to content

feat: OAuth browser login with automatic token lifecycle (0.3.1)#93

Open
Episkey-G wants to merge 4 commits into
ucloud:masterfrom
Episkey-G:oauth-browser-login
Open

feat: OAuth browser login with automatic token lifecycle (0.3.1)#93
Episkey-G wants to merge 4 commits into
ucloud:masterfrom
Episkey-G:oauth-browser-login

Conversation

@Episkey-G

Copy link
Copy Markdown

Summary

Adds browser-based OAuth login to ucloud-cli, so interactive users no longer need to handle AK/SK keys:

  • ucloud auth login — opens the UCloud authorization page; the callback is captured automatically via an RFC 8252 loopback server on 127.0.0.1 (no copy-paste). --no-browser prints the URL for SSH/headless use and falls back to manual paste, as does a 3-minute capture timeout.
  • ucloud auth logout — removes local tokens only; stored AK/SK keys are kept and restored.
  • After login the CLI auto-configures default region/zone/project, validating any pre-existing project_id against the logged-in account.

Credential model

auth_mode on a profile selects exactly one credential mechanism per request:

  • OAuth profiles send Authorization: Bearer only — signature parameters are never emitted.
  • AK/SK and CloudShell signing paths are byte-identical to before (pinned by tests).
  • --public-key/--private-key flags still take precedence and use signing.
  • One profile = one method; scripts/CI keep using AK/SK profiles (ucloud auth login refuses non-interactive terminals with a pointer to AK/SK).

Token lifecycle (fully automatic)

  • Proactive refresh on use, with a 5-minute clock-skew margin before expiry.
  • Reactive recovery: if the gateway rejects a token mid-command (RetCode 174), the CLI refreshes and replays the request exactly once.
  • Refresh-token rotation is serialized across concurrent processes with a file lock + reread-after-lock, so two processes can never burn the same single-use refresh token; other profiles' rotated tokens are merged from disk before saving.
  • Config/credential files are written atomically (same-dir temp file + fsync + rename, mode 0600).

Robustness / security

  • Token values redacted across all log sinks and the panic path; config list shows an AuthMode column and never prints tokens.
  • Token HTTP client honors HTTPS_PROXY/HTTP_PROXY/NO_PROXY (pinned by test).
  • Fixed along the way: config update --base-url validated against the old gateway (impossible to recover from a wrong base-url); init on an OAuth profile didn't persist the switch back to AK/SK; /dev/null stdin was misdetected as an interactive terminal; AggConfigManager held process-lifetime file handles that would break os.Rename on Windows.

Testing

  • Unit/integration: full ./base/ suite incl. concurrency tests under -race (single rotation under concurrent refresh, replay-once guarantees, negative paths).
  • Black-box CLI matrix (tests/oauth_cli_matrix.sh, sandbox HOME): non-TTY fail-fast, missing-token guidance, logout idempotence, OAuth/AK-SK profile coexistence, init switch-back persistence — 8/8.
  • Live verification against production: login (loopback auto-capture), silent renewal of an expired token, reactive 174 refresh-and-replay, full uhost create/stop/start/delete lifecycle over Bearer, web-console logout independence.
  • AK/SK regression: existing prompts and exit codes preserved (asserted in the matrix).

Notes for reviewers

  • The embedded OAuth client credentials follow the public-client convention used by gcloud/gh: a CLI binary cannot keep a secret, and flow security rests on the state check and loopback redirect.
  • vendor/ adds github.com/gofrs/flock v0.8.1 (file locking).
  • Reactive replay triggers only on RetCode 174 ("Token Not Exists" — covers both invalid and expired tokens, verified against the live gateway); auth failures arrive as HTTP 200 + RetCode, so HTTP 401 handling is kept only as a defensive branch.

🤖 Generated with Claude Code

Episkey-G and others added 4 commits June 12, 2026 16:57
Add 'ucloud auth login' / 'ucloud auth logout' implementing OAuth 2.0
authorization-code flow with RFC 8252 loopback auto-capture of the
callback (--no-browser prints the URL and falls back to manual paste).
auth_mode on the profile picks exactly one credential mechanism per
request: oauth profiles send Authorization: Bearer only (stored AK/SK
stay inert for logout restore), AK/SK and CloudShell signing paths are
byte-identical to before.

Token lifecycle is fully automatic: proactive refresh before expiry
(5-minute clock-skew margin), reactive refresh-and-replay-once when the
gateway rejects a token mid-command (RetCode 174), refresh rotation
serialized across processes with a file lock plus reread-after-lock,
and credential/config files written atomically (temp+fsync+rename).
Token values are redacted across all log sinks and the panic path.

Also fixes: config update --base-url validated against the old gateway
(chicken-and-egg), login kept a stale project_id from another account,
init on an oauth profile did not persist the switch back to AK/SK,
/dev/null was misdetected as an interactive terminal, and
AggConfigManager held process-lifetime file handles that break
os.Rename on Windows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…arning; redact oauth error desc

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Episkey-G

Copy link
Copy Markdown
Author

Self-review follow-ups addressed in the latest push:

  • Callback server no longer aborts on noise requests (28b8587): a request to the loopback callback with a missing code or mismatched state now gets HTTP 400 and the CLI keeps waiting for the genuine callback (3-minute timeout unchanged). Only an explicit OAuth error (e.g. access_denied) or a valid code+state terminates the wait. Covered by new tests: noise → 400 + no delivery → subsequent valid callback still succeeds.
  • Login post-save warning: if persisting the auto-configured region/project fails after tokens were already saved, the CLI now prints a warning with a ucloud config update hint instead of a generic error before the success line.
  • error_description from the OAuth server is redacted before being echoed in error messages.
  • Dropped internal verification scaffolding (hack/t0) from the branch (5f6104b).

All gates re-run green: full ./base/ + ./cmd/ tests, black-box CLI matrix 8/8, gofmt clean.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant